<section>
<p>本章节将通过一个小项目来演示一下如何利用scrapy来编写爬虫。我们打算爬取quotes.toscrape.com，这是一个列出名人名言的网站。本篇教程中将带您完成下列任务:</p>
<ul>
<li>创建一个Scrapy项目</li>
<li>编写一只spider爬取网站并提取数据</li>
<li>使用命令行导出爬取的数据</li>
<li>修改spider来递归地跟踪链接</li>
<li>使用spider的参数</li>
</ul>
<h4>创建项目</h4>
<p>在开始编写爬虫爬取数据之前，你必须创建一个新的Scrapy项目。新建目录tutorial_dir并进入这个目录，在命令行下运行:</p>
<pre>
scrapy startproject tutorial
</pre>
<p>该命令将会创建包含下列内容的 tutorial 目录:</p>
<pre>
tutorial/
    scrapy.cfg            # 用于部署项目的配置文件, 基本不用改
    tutorial/             # 项目的python代码目录
        __init__.py
        items.py          # 定义item的文件
        pipelines.py      # 定义pipeline的文件
        settings.py       # 项目的配置文件
        spiders/          # 定义spider(爬虫)的目录
            __init__.py
</pre>
<p>Spider是包含了爬虫工作逻辑的类。它们必须继承scrapy.Spider并定义初始请求(种子url, 爬虫从这些种子url开始爬网页)。这是我们第一个Spider的代码。将它保存在你的项目中的tutorial/spiders目录下的名为dmoz_spider.py文件中：</p>
<pre>
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)
</pre>
<p>正如前面提到的，我们的Spider类继承了scrapy.Spider并定义了一些属性和方法︰</p>
<ul>
    <li>name：标识这个Spider。在一个项目中，它必须是唯一的，也就是，你不能给不同的Spider设置相同的名称。</li>
    <li>start_requests()：必须返回Request的一个可迭代对象（你可以返回一个请求的列表或者编写一个生成器函数），Spider将从它们开始爬取。后续请求将从这些初始的请求连续生成。</li>
    <li>parse()：用来解析每个请求爬回来的网页。response参数是TextResponse的一个实例，它保存页面内容并具有进一步处理它的有用方法。</li>
</ul> 
<p>运行spider</p>
<p>为了让我们的Spider工作，回到项目的顶层目录并运行︰</p>
<pre>scrapy crawl quotes</pre>
<p>此命令会运行我们刚添加的名为quotes的Spider，这个spider会请求quotes.toscrape.com这个域名下的一些网页。输出如下:</p>
<pre>
... (omitted for brevity)
2016-09-20 14:48:00 [scrapy] INFO: Spider opened
2016-09-20 14:48:00 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min)
2016-09-20 14:48:00 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023
2016-09-20 14:48:00 [scrapy] DEBUG: Crawled (404) <GET http://quotes.toscrape.com/robots.txt> (referer: None)
2016-09-20 14:48:00 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
2016-09-20 14:48:01 [quotes] DEBUG: Saved file quotes-1.html
2016-09-20 14:48:01 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/2/> (referer: None)
2016-09-20 14:48:01 [quotes] DEBUG: Saved file quotes-2.html
2016-09-20 14:48:01 [scrapy] INFO: Closing spider (finished)
</pre>
<p>现在，检查当前目录中的文件。你应该注意到已创建两个新文件︰quotes-1.html和quotes-2.html，内容分别对应我们parse方法指示的URL。</p>
<p>运行刚才的命令,Scrapy会调用这个Spider的start_requests方法。在收到服务器的返回后，它实例化Response对象，调用与请求关联的回调方法(默认为parse), 把Response作为参数传递给回调方法。</p>

<p>start_requests方法的快捷方式</p>
<p>可以在spider中只定义一个start_urls的类属性，这样就不用再定义start_requests方法了。scrapy会自动的去请求定义在start_urls中的url，并把response对象传给默认的回调函数(parse)</p>
<pre>
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
</pre>
<p>parse()方法将被调用来处理请求这些URL所返回的数据，尽管我们还没有明确告知Scrapy要这样做。它能够发生是因为parse()是Scrapy的默认回调方法。</p>
<p><b>提取数据</b></p>
<p>学习如何使用Scrapy提取数据的最佳方法就是使用Scrapy shell来试用Selector。运行︰</p>
<pre>scrapy shell 'http://quotes.toscrape.com/page/1/'</pre>
<p>这条命令将会打开一个类似于python交互模式的控制台(下面示例), 另外，记住，从命令行运 Scrapy shell时，要始终将URL放入引号中，否则包含参数的URL（即&字符）将不能工作</p>
<pre>
[ ... Scrapy log here ... ]
2016-09-19 12:09:27 [scrapy] DEBUG: Crawled (200) <GET http://quotes.toscrape.com/page/1/> (referer: None)
[s] Available Scrapy objects:
[s]   scrapy     scrapy module (contains scrapy.Request, scrapy.Selector, etc)
[s]   crawler    <scrapy.crawler.Crawler object at 0x7fa91d888c90>
[s]   item       {}
[s]   request    <GET http://quotes.toscrape.com/page/1/>
[s]   response   <200 http://quotes.toscrape.com/page/1/>
[s]   settings   <scrapy.settings.Settings object at 0x7fa91d888c10>
[s]   spider     <DefaultSpider 'default' at 0x7fa91c8af990>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local objects
[s]   view(response)    View response in a browser
>>>
</pre>
<p>使用shell，你可以尝试使用CSS选择页面的元素︰</p>
<pre>
>>> response.css('title')
[<Selector xpath='descendant-or-self::title' data='<title>Quotes to Scrape</title>'>]
</pre>
<p>运行response.css('title')的返回值是一个称为SelectorList的类似列表的对象，它表示一个Selector对象的列表，这些对象封装XML/HTML元素并允许你进一步查询细粒度的选择或提取数据。</p>
<pre>
>>> response.css('title::text').extract()
['Quotes to Scrape']
</pre>
<p>这里有两件事要注意︰ 一是我们已经添加 ::text到 CSS 查询，意味着我们只想要选择直接在<title>元素内的文本元素。如果我们不指定::text，我们会得到完整的标题元素，包括其标记︰</p>
<pre>
>>> response.css('title').extract()
['<title>Quotes to Scrape</title>']
</pre>
<p>另一件事是调用.extract()的结果是一个列表，因为我们处理的是一个SelectorList实例。当你知道你只是想要第一个结果，就像这种情况，你可以︰</p>
<pre>
>>> response.css('title::text').extract_first()
'Quotes to Scrape'
#  或者
>>> response.css('title::text')[0].extract()
'Quotes to Scrape'
</pre>
<p>却别在于使用extract_first()可以避免IndexError并在找不到任何匹配的元素时返回None。</p>
<p>这里有条经验︰对于大部分爬取代码，你想让它适应在网页上找不到内容而导致的错误，这样即使某些部分爬取失败，你至少可以得到一些数据。</p>
<p>除了extract()和extract_first()方法，你还可以使用re()方法提取，它利用正则表达式来提取数据︰</p>
<pre>
>>> response.css('title::text').re(r'Quotes.*')
['Quotes to Scrape']
>>> response.css('title::text').re(r'Q\w+')
['Quotes']
>>> response.css('title::text').re(r'(\w+) to (\w+)')
['Quotes', 'Scrape']
</pre>
<p>为了找到合适的CSS选择器来使用，你可以在shell中使用view(response)来在网页浏览器中打开资源页。你可以使用你的浏览器开发者工具或扩展有如Firebug.</p>
<p><b>XPATH简介</b></p>
<p>除了CSS，Scrapy选择器还支持使用XPath表达式: 关于xpath， 也可以参考<a href="http://www.w3school.com.cn/xpath/index.asp">这里</a></p>
<pre>
>>> response.xpath('//title')
[<Selector xpath='//title' data='<title>Quotes to Scrape</title>'>]
>>> response.xpath('//title/text()').extract_first()
'Quotes to Scrape'
</pre>
<p>XPath表达式非常强大，而且是Scrapy选择器的基础。事实上，CSS选择器在底层被转换为XPath。如果你仔细查看shell中选择器对象的文本表示形式，你可以看到这点。虽然也许不像CSS选择器受欢迎，XPath表达式提供更强大的能力，因为除了浏览结构，它也可以查看内容。使用XPath，你能够选择这样的东西︰选择包含文本"Next Page"的链接。这使得XPath非常适于爬取任务，我们鼓励你学会XPath，即使你已经知道如何构建CSS选择器，它将使得爬取容易得多。
</p>
<p>提取名言和作者</p>
<p>回到我们的示例项目，现在我们来提取名人名言及其作者</p>
<p>http://quotes.toscrape.com中的每个名言由像这样的HTML元素表示:</p>
<pre>
<span><</span>div class="quote"<span>></span>
    <span><</span>span class="text">“The world as we have created it is a process of our
    thinking. It cannot be changed without changing our thinking.”<span><</span>/span<span>></span>
    <span><</span>span<span>></span>
        by <span><</span>small class="author"<span>></span>Albert Einstein<span><</span>/small<span>></span>
        <span><</span>a href="/author/Albert-Einstein"<span>></span>(about)<span><</span>/a<span>></span>
    <span><</span>/span<span>></span>
    <span><</span>div class="tags"<span>></span>
        Tags:
        <span><</span>a class="tag" href="/tag/change/page/1/"<span>></span>change<span><</span>/a<span>></span>
        <span><</span>a class="tag" href="/tag/deep-thoughts/page/1/"<span>></span>deep-thoughts<span><</span>/a<span>></span>
        <span><</span>a class="tag" href="/tag/thinking/page/1/"<span>></span>thinking<span><</span>/a<span>></span>
        <span><</span>a class="tag" href="/tag/world/page/1/"<span>></span>world<span><</span>/a<span>></span>
    <span><</span>/div<span>></span>
<span><</span>/div<span>></span>
</pre>
<p>让我们打开scrapy shell并把玩一会儿来找出如何提取我们想要的数据:</p>
<pre>
$ scrapy shell 'http://quotes.toscrape.com'
</pre>
<p>我们获得名言HTML元素选择器的一个列表:</p>
<pre>
>>> response.css("div.quote")
</pre>
<p>上述每个查询所返回的选择器允许我们在它们的子元素上运行进一步查询。让我们分配第一个选择器给一个变量，以便我们可以直接在一个特定的名言上运行我们的CSS选择器:</p>
<pre>>>> quote = response.css("div.quote")[0]</pre>
<p>现在，让我们使用我们刚刚创建的quote对象，从中提取title、author和tags:</p>
<pre>
>>> title = quote.css("span.text::text").extract_first()
>>> title
'“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'
>>> author = quote.css("small.author::text").extract_first()
>>> author
'Albert Einstein'
</pre>
<p>鉴于标记是一个字符串列表，我们可以使用 .extract()方法来得到他们全部:</p>
<pre>
>>> tags = quote.css("div.tags a.tag::text").extract()
>>> tags
['change', 'deep-thoughts', 'thinking', 'world']
</pre>
<p>已经弄清楚了如何提取每一位，我们可以迭代提取所有的名言元素并将它们合并到一个Python字典中：</p>
<pre>
>>> for quote in response.css("div.quote"):
...     text = quote.css("span.text::text").extract_first()
...     author = quote.css("small.author::text").extract_first()
...     tags = quote.css("div.tags a.tag::text").extract()
...     print(dict(text=text, author=author, tags=tags))
{'tags': ['change', 'deep-thoughts', 'thinking', 'world'], 'author': 'Albert Einstein', 'text': '“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”'}
{'tags': ['abilities', 'choices'], 'author': 'J.K. Rowling', 'text': '“It is our choices, Harry, that show what we truly are, far more than our abilities.”'}
    ... a few more of these, omitted for brevity
>>>
</pre>
<p><b>在spider中提取数据</b></p>
<pre>
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').extract_first(),
                'author': quote.css('span small::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }
</pre>
<p>如果你运行这个Spider，它将输出提取的数据(例):</p>
<pre>
2016-09-19 18:57:19 [scrapy] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['life', 'love'], 'author': 'André Gide', 'text': '“It is better to be hated for what you are than to be loved for what you are not.”'}
2016-09-19 18:57:19 [scrapy] DEBUG: Scraped from <200 http://quotes.toscrape.com/page/1/>
{'tags': ['edison', 'failure', 'inspirational', 'paraphrased'], 'author': 'Thomas A. Edison', 'text': "“I have not failed. I've just found 10,000 ways that won't work.”"}
</pre>
<p>存储爬取数据</p>
<p>存储爬取的数据的最简单方式是使用Feed exports，使用下面的命令：</p>
<pre>scrapy crawl quotes -o quotes.json</pre>
<p>这将生成一个quotes.json文件，包含所有爬取的项目，以JSON序列化。由于历史原因，Scrapy将追加到一个给定的文件，而不是覆盖其内容。如果你运行此命令两次而没有在第二次之前删除该文件，最终你将得到一个损坏的JSON文件。</p>
<p><b>跟踪链接</b></p>
<p>我们说，你想要爬取网站中的所有页面的名言，而不只是爬取http://quotes.toscrape.com前两页的东西。现在，你知道如何从网页中提取数据，让我们看看如何追踪他们的链接。第一件事是提取我们想要追随的页面的链接。检查我们的页面，我们可以看到有一个用以下标记的到下一页的链接︰
</p>
<pre>
<ul class="pager">
    <li class="next">
        <a href="/page/2/">Next <span aria-hidden="true">&rarr;</span></a>
    </li>
</ul>
</pre>
<p>我们可以尝试在shell中提取它, Scrapy支持一个CSS扩展，让我们可以选择的属性内容，像这样:</p>
<pre>
>>> response.css('li.next a::attr(href)').extract_first()
'/page/2/'
</pre>
<p>让我们看看现在我们的Spider修改为递归地跟踪链接到下一页，从中提取数据:</p>
<pre>
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').extract_first(),
                'author': quote.css('span small::text').extract_first(),
                'tags': quote.css('div.tags a.tag::text').extract(),
            }

        next_page = response.css('li.next a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)
</pre>
<p>现在，在提取数据后，parse方法查找指向下一个页面的链接，使用urljoin()方法生成一个完整的绝对URL（因为链接可能是相对的）并产生一个到下一页的新请求，将自己注册为回调以处理下一个页面的数据提取和保持爬取经过所有的页面。这里你看到的是Scrapy跟踪链接的机制︰当你在回调方法中yield一个Request，Scrapy将调度发送这个Request并注册一个在Request完成时执行的回调方法。</p>
<p>另一个例子</p>
<pre>
import scrapy


class AuthorSpider(scrapy.Spider):
    name = 'author'

    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        # follow links to author pages
        for href in response.css('.author a::attr(href)').extract():
            yield scrapy.Request(response.urljoin(href),
                                 callback=self.parse_author)

        # follow pagination links
        next_page = response.css('li.next a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)

    def parse_author(self, response):
        def extract_with_css(query):
            return response.css(query).extract_first().strip()

        yield {
            'name': extract_with_css('h3.author-title::text'),
            'birthdate': extract_with_css('.author-born-date::text'),
            'bio': extract_with_css('.author-description::text'),
        }
</pre>
<p>这个Spider演示的另一个有趣的事情是，即使有许多名言来自同一作者，我们不需要担心多次前往同一作者页。默认情况下，Scrapy过滤掉已经访问过的URL的重复请求，从而避免因为一个编程错误访问服务器次数太多。这可以通过设置DUPEFILTER_CLASS配置。</p>

<p><b>使用spider的参数</b></p>
<p>当运行你的Spider时，你可以通过使用-a选项来提供命令行参数给它们:</p>
<pre>scrapy crawl quotes -o quotes-humor.json -a tag=humor</pre>
<p>这些参数将传递给Spider的__init__ 方法，默认情况下成为Spider的属性。在此示例中，提供给tag参数的值，可通过self.tag访问。基于参数构建URL，你可以使用它来让你的Spider只获取带有一个特定标记的名言：</p>
<pre>
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        url = 'http://quotes.toscrape.com/'
        tag = getattr(self, 'tag', None)
        if tag is not None:
            url = url + 'tag/' + tag
        yield scrapy.Request(url, self.parse)

    def parse(self, response):
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').extract_first(),
                'author': quote.css('span small a::text').extract_first(),
            }

        next_page = response.css('li.next a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, self.parse)
</pre>
<p>如果你传递tag=humor参数给这个Spider，你会注意到它将只访问来自humor标记的URL，如http://quotes.toscrape.com/tag/humor。</p>
<p>
学习的最好方法就是参考例子，Scrapy也不例外。有一个名为quotesbot的Scrapy示例项目，你可以用来把玩和了解关于Scrapy的更多内容。它包含两个针对http://quotes.toscrape.com的Spider，一个使用CSS选择器，另一个使用XPath表达式。
</p>
</section>